const fs = require('fs-extra');
const path = require('path');
const Excel = require('exceljs');
const sizeOf = require('image-size');
const _ = require('lodash');
const chalk = require('chalk');
const inliner = require('inliner');
const hljs = require('highlight.js');
const CWD = process.cwd();
const heading = [];
let config, pages;

function writeToFile(url, distFile) {
    return new Promise(resolve => {
        new inliner(url, { images: true, inlinemin: true }, function (error, html) {
            if (error) {
                console.log(chalk.red(event));
            } else {
                // console.log('start write to:', distFile);
                if (_.isFunction(distFile)) {
                    distFile(html)
                } else {
                    fs.writeFileSync(distFile, html);
                    console.log('write to:', distFile);
                }
            }
            resolve();
        })
        .on('progress', function (event) {
            // console.log(chalk.green(event));
        });
    });
}
async function parseExcelData(excelFile, children, file) {
    const getValues = (ws, rowNo) => {
        const values = ws.getRow(rowNo).values.slice(1);
        return _.map(values, (o, index) => {
            if (o && o.richText) {
                return _.map(o.richText, m => m.text).join('');
            }
            if (o && o.formula) {
                return o.result;
            }
            return o;
        }).map(o => o && o.trim ? o.trim() : o);
    };
    const getAlignments = (ws, colCount, rowNo) => {
        const row =  ws.getRow(rowNo);
        const alignments = [];
        for (let i=1; i<=colCount; i++) {
            const style = row.getCell(i).style;
            if (!style.alignment && !style.font) {
                alignments.push('right');
            } else if (!style.alignment) {
                alignments.push('left');
            } else {
                alignments.push(style.alignment.horizontal||'left');
            }
        }
        return alignments;
    };
    const getWidths = (ws, colCount) => {
        const tw = config.pageSize.w; // 总宽度
        let widths = [];
        for (let i=0; i<colCount; i++) {
            widths.push(ws.columns[i].width||10);
        }
        const totalWidth = _.sum(widths);
        widths = widths.map(o=>Math.round(o/totalWidth*tw)-4);
        return widths;
    }
    const dir = path.dirname(file);
    const wb = new Excel.Workbook();
    await wb.xlsx.readFile(path.join(dir, excelFile));
    // const ws = workbook.getWorksheet('My Sheet');
    const ws = wb.worksheets[0];
    const rowCount = ws.actualRowCount;
    const colCount = ws.columnCount;
    const alignments = getAlignments(ws, colCount, 2);
    const widths = getWidths(ws, colCount);

    const text = (str, width, alignment = 'center') => ({ text: str, fontSize: config.tableFontSize, fontName: config.fontName, alignment, width });
    const header = getValues(ws, 1).map((o, i)=>text(o, widths[i]));
    const rows = [];
    for (var i = 2; i <= rowCount; i++) {
        rows.push(getValues(ws, i).map((o, i)=> text(o, widths[i], alignments[i])));
    }
    children.push({ table: { header, rows }, file  });
}
function parseImage(line, list = []) {
    const match = line.match(/\s*!\[([^\]]*)\]\(([^)]*)\)\s*/);
    if (match) {
        const text = match[1];
        const image = match[2];
        list.push({ text, image });
        line = line.replace(/\s*!\[([^\]]*)\]\(([^)]*)\)\s*/, '');
        if (line) {
            return parseImage(line, list);
        }
    }
    return list;
}
function createImage(list, children, file) {
    console.log('[createImage]', list[0]);
    let w, h;
    const tw = config.pageSize.w; // 总宽度
    const dir = path.dirname(file);
    if (list.length === 1) {
        w = tw;
        const size = sizeOf(path.join(dir, list[0].image));
        h = w * size.height / size.width;
    } else {
        w = tw / list.length;
        h = w * _.meanBy(_.map(list, o=>sizeOf(path.join(dir, o.image))), o=>o.height/o.width);
    }
    const images = list.map(o=>({ img: path.join('/', dir.replace(config.rootDir, ''), o.image), text: o.text, w, h }));
    children.push({ images, file, w: tw, h, lineHeight: Math.min(h, Math.floor(config.pageSize.h)) });
}
function createExcel(excelTextList, children, file) {
    console.log('[createExcel]', excelTextList[0]);
    const tw = config.pageSize.w; // 总宽度
    const list = (line) => line.replace(/^\s*\|/, '').replace(/\|\s*$/, '').split('|').map(o=>o.trim().replace(/<br>/g, '\n'));
    const text = (str, width, alignment = 'center') => ({ text: str, fontSize: config.tableFontSize, fontName: config.fontName, alignment, width });
    const row = (line, widths = [], alignments = []) => list(line).map((o, i)=> text(o, widths[i], alignments[i]));
    const alignments = list(excelTextList[1]).map(o=> /^:\s*\d+:\s*$/.test(o) ? 'center' : /\d+\s*:$/.test(o) ? 'right' : 'left');
    let widths = list(excelTextList[1]).map(o=>+(o.replace(/[:\s]*/g, ''))||1);
    const totalWidth = _.sum(widths);
    widths = widths.map(o=>Math.round(o/totalWidth*tw)-4);
    const header = row(excelTextList[0], widths);
    const rows = excelTextList.slice(2).map(line=>row(line, widths, alignments));
    children.push({ table: { header, rows }, file, lineHeight: 26  });
}
function createCode(codeTextList, children, languageName, file) {
    console.log('[createCode]', codeTextList[0]);
    children.push({ languageName, code: codeTextList.join('\n'), file });
}
async function crateWordLayer(dir, children, level = -1) {
    const info = fs.statSync(dir);
    if (info.isDirectory()) {
        const files = _.sortBy(fs.readdirSync(dir), parseInt);
        for (const index in files) {
            const file = files[index];
            const fullname = path.join(dir, file);
            const info = fs.statSync(fullname);
            if(info.isDirectory()) {
                await crateWordLayer(fullname, children, level + 1);
            } else if (/.*\.md/.test(file)) {
                await parseMarkDownFile(fullname, level, children);
            }
        }
    } else {
        await parseMarkDownFile(dir, level, children);
    }
}
async function getChildren(dir) {
    const children = [];
    await crateWordLayer(dir, children);
    const count = children.length-1;
    for (let i=count; i>0; i--) {
        children[2*i] = children[i];
        children[2*i-1] = {
            text: '',
            fontSize: config.fontSize,
            lineHeight: 26,
        };
    }
    return children;
}
async function parseMarkDownFile(file, level, children) {
    const text = fs.readFileSync(file, 'utf8');
    const list = text.split(/\n/).filter(Boolean);
    let isExcel = false, isCode = false, languageName = 'auto';
    let excelTextList = [];
    let codeTextList = [];
    for (const line of list) {
        if (/^\s*\|.*\|\s*$/.test(line)) { // 表格
            isExcel = true;
            excelTextList.push(line.trim());
        } else if (/^\s*```.*$/.test(line)) { // 代码
            if (!isCode) {
                isCode = true;
                languageName = line.replace(/^\s*```\s*(.*)\s*$/, '$1');
            } else {
                isCode = false;
                // 生成代码片段
                createCode(codeTextList, children, languageName, file);
                codeTextList = [];
            }
        } else {
            if (isExcel) {
                isExcel = false;
                // 生成表格
                createExcel(excelTextList, children, file);
                excelTextList = [];
            }
            if (isCode) { // 插入代码
                codeTextList.push(line);
            } else if (/^#+\s+/.test(line)) { // 标题
                const li = line.replace(/(^#+)[^#]*/, '$1');
                const no = li.length + Math.max(level, 0);
                const title = line.replace(/^#+\s+/, '');
                console.log(`HEADING_${no}:${_.repeat('-', no*3)}${title}`);
                heading[no-1] = (heading[no-1] || 0) + 1;
                heading.length = no;
                children.push({
                    file,
                    level,
                    text: `${heading.join('.')} ${title}`,
                    headingNo: no,
                    fontSize: config.headingFontSizes[no-1],
                    lineHeight: config.headingFontSizes[no-1],
                });
            } else if (/^\s*!\[([^\]]*)\]\(([^)]*)\)/.test(line)) { // 图片
                const list = parseImage(line, []);
                createImage(list, children, file);
            } else if (/^\s*!<[^>]*>/.test(line)) { // excel
                await parseExcelData(line.replace(/^\s*!<([^>]*)>.*/, '$1'), children, file);
            } else if (/^\*+\s+/.test(line)) { // 列表
                console.log('[createList]', line);
                const li = line.replace(/(^\*+)[^*]*/, '$1');
                const title = line.replace(/^\*+\s+/, '');
                const head = [ '', '■', '&emsp;&emsp;◆', '&emsp;&emsp;&emsp;&emsp;●' ][li.length];
                children.push({
                    file,
                    text: `${head} ${title}`,
                    fontSize: config.fontSize,
                    fontName: config.fontName,
                    lineHeight: 26,
                });
            } else {
                children.push({
                    file,
                    text: line,
                    fontSize: config.fontSize,
                    fontName: config.fontName,
                    lineHeight: 26,
                });
            }
        }
    }
    if (isExcel) {
        isExcel = false;
        // 生成表格
        createExcel(excelTextList, children, file);
        excelTextList = [];
    }
}
function getHtml(children) {
    let imgNo = 1;
    return children.map((o, i)=>{
        if (o.headingNo) {
            return `<h${o.headingNo} id="item${i}" style="font-size: ${o.fontSize}px;">${o.text}</h${o.headingNo}>`;
        } else if (o.images) {
            return `<div id="item${i}" class="imageRow" style="width:${o.w}px;height:${o.h}px;">${o.images.map(m=>`<div class="imageItem"><img src="${m.img}" style="width:${m.w}px;height:${m.h}px;"/>${m.text?`<div class="imageItem">图${imgNo++}：${m.text}</div>`:''}</div>`).join('')}</div><br/>`;
        } else if (o.table) {
            return `<table id="item${i}"><thead><tr>${o.table.header.map(m=>`<th style="text-align:${m.alignment};min-width:${m.width}px;max-width:${m.width}px">${m.text}</th>`).join('')}</tr></thead><tbody>${o.table.rows.map(m=>`<tr>${m.map(n=>`<td style="text-align:${n.alignment}">${n.text}</td>`).join('')}</tr>`).join('')}</tbody></table>`;
        } else if (o.code) {
            return `<pre id="item${i}"><code class="hljs">${hljs.highlight(o.languageName, o.code).value}</code></pre>`;
        } else {
            return `<p id="item${i}" style="font-size: ${o.fontSize}px; line-height: ${o.lineHeight}px;">&emsp;&emsp;${o.text}</p>`;
        }
    }).join('');
}
function getPageHtml(children, pages, pageIndex) {
    pageIndex = pageIndex - 3;
    const page = pages[pageIndex];
    if (!page) {
        return `<div></div>`;
    }
    const offset = _.get(config.offset, pageIndex+1, '');
    let offsetTop = 0, offsetHeight = 0;
    if (offset) {
        const list = offset.split(/,|，/);
        offsetTop = +list[0] || 0;
        offsetHeight = (+list[1] || 0)+offsetTop;
    }
    const list = _.map(page, o=>children[o.i]);
    const height = _.get(_.last(page), 'h', config.pageSize.h) + offsetHeight;
    const top = _.get(_.first(page), 't', 0) + offsetTop;
    const style = top < 0 ? ` style="top:${top}px"` : '';

    return `
    <div class="book-content-container" style="height:${height}px">
    <div class="book-content"${style}>
    ${getHtml(list)}
    </div>
    </div>
    <span class="page-number">${+pageIndex+1}</span>
    `;
}
async function startBook(configPath, port, verbose, open, build) {
    if (/\.md$/.test(configPath)) {
        config = {
            srcPath: configPath,
        }
    } else {
        config = require(path.resolve(CWD, configPath));
    }
    config = {
        pageSize: { w: 400, h: 520 },
        backgroundColor: 'FFFFFF', // 背景色
        fontName: '宋体', // 字体的名称
        fontSize: 28, // 字体大小
        tableFontSize: 28, // 表格的文字的字体大小
        headingFontSizes: [32, 24, 20, 16, 14, 13.6], // heading 的字体大小
        ...config
    };
    config.fontSize = config.fontSize / 2;
    config.tableFontSize = config.tableFontSize / 2;
    config.rootDir = path.dirname(config.srcPath);
    let children = await getChildren(config.srcPath);
    const pageHtmlList = {};
    const distPath = path.join(CWD, config.distPath);
    const express = require('express');
    const bodyParser = require('body-parser');
    const morgan = require('morgan');
    const app = express();
    verbose && app.use(morgan('short'));
    app.use(bodyParser.json());
    if (config.staticDir) {
        app.use('/', express.static(config.staticDir));
        if (build) {
            app.use('/pages', express.static(config.staticDir));
        }
    }
    app.use(express.static(config.rootDir));
    app.use(express.static(path.join(__dirname, 'res')));
    config.rootDir === '.' && (config.rootDir = '');

    app.get('/', (req, res) => {
        const { pageIndex, o, build } = req.query;
        res.send(fs.readFileSync(path.join(__dirname, 'res/book/index.test.html'), 'utf-8').replace('<%pageIndex%>', pageIndex).replace('<%o%>', o).replace('<%build%>', build).replace('<%children%>', JSON.stringify(children)));
    });
    app.get('/main.html', (req, res) => {
        const { pageCount, build } = req.query;
        const list = getHtml(children);
        let html = fs.readFileSync(path.join(__dirname, 'res/book/index.template.html'), 'utf-8').replace(/<%pageCount%>/g, pageCount).replace(/<%preEndPage%>/g, pageCount-1).replace('<%build%>', build).replace('<%cover%>', config.cover);
        if (build) {
            html = html.replace('<%children%>', list);
        }
        res.send(html);
    });
    app.get('/pages/*.html', (req, res) => {
        const pageIndex = _.last(req.url.split('/')).replace('.html', '');
        const html = getPageHtml(children, pages, pageIndex);
        res.send(html);
    });
    app.get('/pages.js', (req, res) => {
        res.send(`var pageHtmlList = ${JSON.stringify(pageHtmlList)}`);
    });
    app.post('/setBook', (req, res) => {
        pages = req.body;
        res.send('success');
    });
    app.post('/build', async (req, res) => {
        const { pageCount, realPageCount } = req.body;
        console.log('/build [recv]:', req.body);
        for (let i=0; i<=realPageCount; i++) {
            await writeToFile(`http://localhost:${port}/pages/${i+3}.html`, html => pageHtmlList[i+3] = html);
        }
        fs.ensureDirSync(distPath);
        await writeToFile(`http://localhost:${port}/main.html?pageCount=${pageCount}&build=1`, path.join(distPath, 'index.html'));
        res.send('已经打包成功，文件在'+distPath);
        setTimeout(()=>{
            process.exit(0);
        }, 500);
    });

    const server = app.listen(port);
    server.on('listening', function () {
        const url = 'http://localhost:' + port;
        console.log('Open', url);
        const gulp = require('gulp');
        const browserSync = require('browser-sync').create();
        if (build) {
            return browserSync.init({
                proxy: `${url}?build=1`,
                notify: false,
                open: true,
            });
        }
        gulp.task('browser', function() {
            browserSync.init({
                proxy: url,
                files: [{
                    match: [path.join(CWD, config.srcPath)+'/**/*.md'],
                    fn: async function (event, file) {
                        if (event === 'change') {
                            const currentChild = _.find(children, o=>o.file===file && o.headingNo);
                            if (currentChild) {
                                const level = currentChild.level;
                                const list = [];
                                await parseMarkDownFile(file, level, list);
                                const start = _.findIndex(children, o=>o.file===file);
                                const end = _.findLastIndex(children, o=>o.file===file);
                                children.splice(start, end-start+1, ...list);
                                browserSync.reload();
                            } else {
                                children = await getChildren(config.srcPath);
                                browserSync.reload();
                            }
                        } else {
                            children = await getChildren(config.srcPath);
                            browserSync.reload();
                        }
                    }
                }],
                notify: false,
                open,
            });
        });
        gulp.start(['browser']);
    });
    server.on('error', function (err) {
        console.log('open error on port:', port);
        startBook(configPath, port+1, verbose, open, build);
    });
}

module.exports = startBook;
